/****************************************************************************
**
** Copyright (C) 2017 The Qt Company Ltd.
** Contact: http://www.qt.io/licensing/
**
** This file is part of the QtSerialBus module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL3$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see http://www.qt.io/terms-conditions. For further
** information use the contact form at http://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPLv3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or later as published by the Free
** Software Foundation and appearing in the file LICENSE.GPL included in
** the packaging of this file. Please review the following information to
** ensure the GNU General Public License version 2.0 requirements will be
** met: http://www.gnu.org/licenses/gpl-2.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#ifndef QMODBUSOVERTCPCLIENT_P_H
#define QMODBUSOVERTCPCLIENT_P_H

#include <QtCore/qloggingcategory.h>
#include <QtCore/qmath.h>
#include <QtCore/qpointer.h>
#include <QtCore/qqueue.h>
#include <QtCore/qtimer.h>
#include <QtNetwork/qhostaddress.h>
#include <QtNetwork/qtcpsocket.h>
#include "QtSerialBus/qmodbusovertcpclient.h"
#include <QtSerialBus/qmodbusrtuserialmaster.h>
#include <QtSerialPort/qserialport.h>

#include "private/qmodbusclient_p.h"

#include <private/qmodbusadu_p.h>
#include <private/qmodbus_symbols_p.h>
//
//  W A R N I N G
//  -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//

QT_BEGIN_NAMESPACE

Q_DECLARE_LOGGING_CATEGORY(QT_MODBUS)
Q_DECLARE_LOGGING_CATEGORY(QT_MODBUS_LOW)

class Timer : public QObject
{
    Q_OBJECT

public:
    Timer() = default;
    int start(int msec)
    {
        m_timer = QBasicTimer();
        m_timer.start(msec, Qt::PreciseTimer, this);
        return m_timer.timerId();
    }
    void stop() { m_timer.stop(); }
    bool isActive() const { return m_timer.isActive(); }

signals:
    void timeout(int timerId);

private:
    void timerEvent(QTimerEvent *event) override
    {
        const auto id = m_timer.timerId();
        if (event->timerId() == id)
            emit timeout(id);
    }

private:
    QBasicTimer m_timer;
};

class QModbusOverTcpClientPrivate : public QModbusClientPrivate
{
    Q_DECLARE_PUBLIC(QModbusOverTcpClient)
    enum State { Idle, WaitingForReplay, ProcessReply } m_state = Idle;

public:
    void onReadyRead()
    {
        m_responseBuffer += m_socket->read(m_socket->bytesAvailable());
        qCDebug(QT_MODBUS_LOW) << "(RTU client) Response buffer:" << m_responseBuffer.toHex();

        if (m_responseBuffer.size() < 2) {
            qCDebug(QT_MODBUS) << "(RTU client) Modbus ADU not complete";
            return;
        }

        const QModbusSerialAdu tmpAdu(QModbusSerialAdu::Rtu, m_responseBuffer);
        int pduSizeWithoutFcode = QModbusResponse::calculateDataSize(tmpAdu.pdu());
        if (pduSizeWithoutFcode < 0) {
            // wait for more data
            qCDebug(QT_MODBUS) << "(RTU client) Cannot calculate PDU size for function code:"
                               << tmpAdu.pdu().functionCode() << ", delaying pending frame";
            return;
        }

        // server address byte + function code byte + PDU size + 2 bytes CRC
        int aduSize = 2 + pduSizeWithoutFcode + 2;
        if (tmpAdu.rawSize() < aduSize) {
            qCDebug(QT_MODBUS) << "(RTU client) Incomplete ADU received, ignoring";
            return;
        }

        if (m_queue.isEmpty())
            return;
        auto &current = m_queue.first();

        // Special case for Diagnostics:ReturnQueryData. The response has no
        // length indicator and is just a simple echo of what we have send.
        if (tmpAdu.pdu().functionCode() == QModbusPdu::Diagnostics) {
            const QModbusResponse response = tmpAdu.pdu();
            if (canMatchRequestAndResponse(response, tmpAdu.serverAddress())) {
                quint16 subCode = 0xffff;
                response.decodeData(&subCode);
                if (subCode == Diagnostics::ReturnQueryData) {
                    if (response.data() != current.requestPdu.data())
                        return; // echo does not match request yet
                    aduSize = 2 + response.dataSize() + 2;
                    if (tmpAdu.rawSize() < aduSize)
                        return; // echo matches, probably checksum missing
                }
            }
        }

        const QModbusSerialAdu adu(QModbusSerialAdu::Rtu, m_responseBuffer.left(aduSize));
        m_responseBuffer.remove(0, aduSize);

        qCDebug(QT_MODBUS) << "(RTU client) Received ADU:" << adu.rawData().toHex();
        if (QT_MODBUS().isDebugEnabled() && !m_responseBuffer.isEmpty())
            qCDebug(QT_MODBUS_LOW) << "(RTU client) Pending buffer:" << m_responseBuffer.toHex();

        // check CRC
        if (!adu.matchingChecksum()) {
            qCWarning(QT_MODBUS) << "(RTU client) Discarding response with wrong CRC, received:"
                                 << adu.checksum<quint16>() << ", calculated CRC:"
                                 << QModbusSerialAdu::calculateCRC(adu.data(), adu.size());
            return;
        }

        const QModbusResponse response = adu.pdu();
        if (!canMatchRequestAndResponse(response, adu.serverAddress())) {
            qCWarning(QT_MODBUS) << "(RTU client) Cannot match response with open request, "
                                    "ignoring";
            return;
        }

        m_state = ProcessReply;
        m_responseTimer.stop();
        current.m_timerId = INT_MIN;

        processQueueElement(response, m_queue.dequeue());

        m_state = Idle;
        scheduleNextRequest(m_interFrameDelayMilliseconds);
    }
    void setupTcpSocket()
    {
        Q_Q(QModbusOverTcpClient);

        m_socket = new QTcpSocket(q);

        QObject::connect(m_socket, &QAbstractSocket::connected, q, [this]() {
            qCDebug(QT_MODBUS) << "(TCP client) Connected to" << m_socket->peerAddress()
                               << "on port" << m_socket->peerPort();
            Q_Q(QModbusOverTcpClient);
            m_responseBuffer.clear();
            q->setState(QModbusDevice::ConnectedState);
        });

        QObject::connect(m_socket, &QAbstractSocket::disconnected, q, [this]() {
            qCDebug(QT_MODBUS) << "(TCP client) Connection closed.";
            Q_Q(QModbusOverTcpClient);
            q->setState(QModbusDevice::UnconnectedState);
            cleanupTransactionStore();
        });

        QObject::connect(m_socket, &QAbstractSocket::errorOccurred, q,
                         [this](QAbstractSocket::SocketError /*error*/) {
                             Q_Q(QModbusOverTcpClient);

                             if (m_socket->state() == QAbstractSocket::UnconnectedState) {
                                 cleanupTransactionStore();
                                 q->setState(QModbusDevice::UnconnectedState);
                             }
                             q->setError(QModbusClient::tr("TCP socket error (%1).")
                                                 .arg(m_socket->errorString()),
                                         QModbusDevice::ConnectionError);
                         });

        QObject::connect(m_socket, &QIODevice::readyRead, q, [this]() { onReadyRead(); });
    }

    QModbusReply *enqueueRequest(const QModbusRequest &request, int serverAddress,
                                 const QModbusDataUnit &unit, QModbusReply::ReplyType type) override
    {
        auto writeToSocket = [this](const QModbusRequest &request, int address) {
//            QByteArray buffer;
//            QDataStream output(&buffer, QIODevice::WriteOnly);
            auto adu = QModbusSerialAdu::create(QModbusSerialAdu::Rtu, address, request);
//            output << adu;

            int writtenBytes = m_socket->write(adu);
            if (writtenBytes == -1 || writtenBytes < adu.size()) {
                Q_Q(QModbusOverTcpClient);
                qCDebug(QT_MODBUS) << "(TCP client) Cannot write request to socket.";
                q->setError(QModbusOverTcpClient::tr("Could not write request to socket."),
                            QModbusDevice::WriteError);
                return false;
            }
            qCDebug(QT_MODBUS_LOW) << "(TCP client) Sent TCP ADU:" << adu.toHex();
            qCDebug(QT_MODBUS) << "(TCP client) Sent TCP PDU:" << request;// << "with tId:" << Qt::hex << tId;
            return true;
        };

        if (!writeToSocket(request, serverAddress))
            return nullptr;

        Q_Q(QModbusOverTcpClient);

        auto reply = new QModbusReply(serverAddress == 0 ? QModbusReply::Broadcast : type,
                                      serverAddress, q);
        QueueElement element(reply, request, unit, m_numberOfRetries + 1);
        element.adu = QModbusSerialAdu::create(QModbusSerialAdu::Rtu, serverAddress, request);
        m_queue.enqueue(element);

        scheduleNextRequest(m_interFrameDelayMilliseconds);

        return reply;
    }

    void scheduleNextRequest(int delay)
    {
        Q_Q(QModbusOverTcpClient);

        if (m_state == Idle && !m_queue.isEmpty()) {
            m_state = WaitingForReplay;
            QTimer::singleShot(delay, q, [this]() { processQueue(); });
        }
    }

    void processQueue()
    {
        m_responseBuffer.clear();
        // m_socket->clear(QSerialPort::AllDirections);

        if (m_queue.isEmpty())
            return;
        auto &current = m_queue.first();

        if (current.reply.isNull()) {
            m_queue.dequeue();
            m_state = Idle;
            scheduleNextRequest(m_interFrameDelayMilliseconds);
        } else {
            current.bytesWritten = 0;
            current.numberOfRetries--;
            m_socket->write(current.adu);

            qCDebug(QT_MODBUS) << "(RTU client) Sent Serial PDU:" << current.requestPdu;
            qCDebug(QT_MODBUS_LOW).noquote()
                    << "(RTU client) Sent Serial ADU: 0x" + current.adu.toHex();
        }
    }
    bool canMatchRequestAndResponse(const QModbusResponse &response, int sendingServer) const
    {
        if (m_queue.isEmpty())
            return false;
        const auto &current = m_queue.first();

        if (current.reply.isNull())
            return false; // reply deleted
        if (current.reply->serverAddress() != sendingServer)
            return false; // server mismatch
        if (current.requestPdu.functionCode() != response.functionCode())
            return false; // request for different function code
        return true;
    }
    // TODO: Review once we have a transport layer in place.
    bool isOpen() const override
    {
        if (m_socket)
            return m_socket->isOpen();
        return false;
    }

    void cleanupTransactionStore()
    {
        if (m_queue.isEmpty())
            return ;

        qCDebug(QT_MODBUS) << "(TCP client) Cleanup of pending requests";

//        for (const auto &elem : qAsConst(m_transactionStore)) {
//            if (elem.reply.isNull())
//                continue;
//            elem.reply->setError(QModbusDevice::ReplyAbortedError,
//                                 QModbusClient::tr("Reply aborted due to connection closure."));
//        }
        m_queue.clear();
    }

    // This doesn't overflow, it rather "wraps around". Expected.
    //inline void incrementTransactionId() { m_transactionId++; }
    //inline int transactionId() const { return m_transactionId; }

    QIODevice *device() const override { return m_socket; }

    Timer m_responseTimer;
    QByteArray m_responseBuffer;
    QQueue<QueueElement> m_queue;
    QTcpSocket *m_socket = nullptr;
    // QHash<quint16, QueueElement> m_transactionStore;
    int mbpaHeaderSize = 2;
    int m_interFrameDelayMilliseconds = 2;
    int m_turnaroundDelay = 100; // Recommended value is between 100 and 200 msec.

private: // Private to avoid using the wrong id inside the timer lambda,
    // quint16 m_transactionId = 0; // capturing 'this' will not copy the id.
};

QT_END_NAMESPACE

#endif // QMODBUOVERSTCPCLIENT_P_H
